/*
 * Copyright (C) 2003, Velocityme Partnership
 * TaskSessionBean.java
 *
 * Created on 12 May 2003, 07:53
 */

package com.velocityme.session;

import com.velocityme.entity.ChangeDeltaItemTypeBean;
import com.velocityme.entity.ChangeDeltaTypeBean;
import com.velocityme.entity.PermissionBean;
import javax.ejb.*;
import javax.naming.*;
import com.velocityme.interfaces.*;
import com.velocityme.utility.InvalidKeyException;
import com.velocityme.utility.InvalidTimeException;
import com.velocityme.utility.PermissionDeniedException;
import com.velocityme.utility.TaskTreeGraph;
import com.velocityme.valueobjects.*;
import java.rmi.RemoteException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.xml.transform.stream.StreamResult;

/**
 *
 * @author  Robert Crida Work
 * @ejb.bean
 *           type="Stateless"
 *           cmp-version="2.x"
 *           name="TaskSession"
 *           jndi-name="ejb/TaskSession"
 *           view-type="local"
 *           transaction-type="Container"
 * @ejb.transaction type="Required"
 *
 * @ejb.util generate="physical"
 */
public class TaskSessionBean implements SessionBean {
    
    private SessionContext m_context;
    
    private SearchSessionLocal m_searchSessionLocal;
    
    private DateFormat m_formatter;

    private PermissionLocal m_permissionNodeView;
    private PermissionLocal m_permissionTaskChangeState;
    private PermissionLocal m_permissionTimeCreate;
    private PermissionLocal m_permissionTimeEdit;
    private PermissionLocal m_permissionTimeDelete;
    private PermissionLocal m_permissionProjectEdit;
    private PermissionLocal m_permissionTaskReassignResponsibility;
    private PermissionLocal m_permissionTaskEditDependency;
    private ChangeDeltaTypeLocal m_changeDeltaTypeChangeState;
    private StateLocal m_startState;

    /**
     * Helper method which performs work involved in creating a task. Designed
     * to be chained with other methods to create a composite object.
     *
     * @param p_key User's security key
     * @param p_parentNode The node which will contain the new Action
     * @param p_actionValue The value details for the Action part of the action
     *
     * @ejb.interface-method view-type="local"
     **/
    public TaskLocal createTaskHelper(KeySessionLocal p_keyLocal,
                               NodeLocal p_parentNodeLocal,
                               com.velocityme.valueobjects.TaskValue p_taskValue,
                               ChangeDeltaLocal p_changeDeltaLocal)
        throws InvalidKeyException {
        try {     
            NodeValue nodeValue = p_taskValue.getNodeValue();
            // start and stop must be equal for anchors, otherwise default values
            if (p_taskValue.getType().equals("Anchor")) {
                p_taskValue.setStopDate(p_taskValue.getStartDate());
            }
            else {
                Date tNow = new Date();
                p_taskValue.setStartDate(tNow);
                long start = tNow.getTime();
                long duration = (long)(p_taskValue.getExpectedDuration_min().intValue() * 60000);
                p_taskValue.setStopDate(new Date(tNow.getTime() + duration));
            }
            p_taskValue.setRemainingDuration_min(p_taskValue.getExpectedDuration_min());
            nodeValue.setType("Task");
            p_taskValue.setCreatorContactableValue(p_keyLocal.getUserLocal().getPersonLocal().getContactableLocal().getContactableValue());
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NAME, nodeValue.getName());
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, nodeValue.getDescription());
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PRIORITY, p_taskValue.getPriority().toString());
            if (p_taskValue.getType().equals("Anchor")) {
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DATE, m_formatter.format(p_taskValue.getStartDate()));
            }
            else {
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERCENTAGE_COMPLETE, p_taskValue.getPercentageComplete().toString());
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.EXPECTED_DURATION, TaskValueToString.expectedDurationToString(p_taskValue));
            }
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PARENT_NODE, p_parentNodeLocal.getPathName());
            NodeLocal nodeLocal = NodeUtil.getLocalHome().create(p_changeDeltaLocal.getDBObjectLocal(), nodeValue, p_parentNodeLocal);
            nodeValue.setNodeId(nodeLocal.getNodeId());
            p_taskValue.setCreatorContactableValue(p_keyLocal.getUserLocal().getPersonLocal().getContactableLocal().getContactableValue());
            TaskLocal taskLocal = TaskUtil.getLocalHome().create(nodeLocal, p_taskValue);
            p_taskValue.setTaskId(taskLocal.getTaskId());
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.CREATOR, taskLocal.getCreatorContactableLocal().getName());
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.OWNER, taskLocal.getOwnerContactableLocal().getName());
            if (!p_taskValue.getType().equals("Anchor")) {
                performStartTransitionLocal(p_keyLocal, taskLocal, p_changeDeltaLocal);
            }
            return taskLocal;
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Helper method which performs task related work when editing. Designed to
     * be chained with other element of conglomerate objects.
     *
     * @param p_key User's security key
     * @param p_action The Action object being editted
     * @param p_actionValue The value details for the Action part of the action
     *
     * @ejb.interface-method view-type="local"
     **/
    public void editTaskHelper(KeySessionLocal p_keyLocal, 
                        TaskLocal p_taskLocal,
                        com.velocityme.valueobjects.TaskValue p_taskValue,
                        java.util.Collection p_responsibleContactableLocals,
                        java.util.Collection p_interestedContactableLocals,
                        ChangeDeltaLocal p_changeDeltaLocal)
        throws InvalidKeyException, NamingException, CreateException {
        NodeLocal nodeLocal = p_taskLocal.getNodeLocal();
        NodeValue nodeValue = p_taskValue.getNodeValue();
        // copy settings for creator, start and stop dates
        p_taskValue.setCreatorContactableValue(p_taskLocal.getCreatorContactableLocal().getContactableValue());
        // start and stop must be equal for anchors, otherwise unchanged
        if (p_taskValue.getType().equals("Anchor")) {
            p_taskValue.setStopDate(p_taskValue.getStartDate());
        }
        else {
            p_taskValue.setStartDate(p_taskLocal.getStartDate());
            p_taskValue.setStopDate(p_taskLocal.getStopDate());
        }
        if (!nodeValue.getName().equals(nodeLocal.getName()))
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NAME, nodeValue.getName());
        if (!nodeValue.getDescription().equals(nodeLocal.getDescription()))
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, nodeValue.getDescription());
        if (!p_taskValue.getPriority().equals(p_taskLocal.getPriority()))
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PRIORITY, p_taskValue.getPriority().toString());
        Integer oldOwnerId = p_taskLocal.getOwnerContactableLocal().getContactableId();
        if (p_taskValue.getType().equals("Anchor")) {
            if (!p_taskValue.getStartDate().equals(p_taskLocal.getStartDate()))
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DATE, m_formatter.format(p_taskLocal.getStartDate()) + " -> " + m_formatter.format(p_taskValue.getStartDate()));                
        }
        else {
            p_taskValue.setStateValue(p_taskLocal.getStateLocal().getStateValue());
            p_taskValue.setStatusValue(p_taskLocal.getStatusLocal().getStatusValue());
            if (!p_taskValue.getPercentageComplete().equals(p_taskLocal.getPercentageComplete()))
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERCENTAGE_COMPLETE, p_taskValue.getPercentageComplete().toString());
            if (!p_taskValue.getExpectedDuration_min().equals(p_taskLocal.getExpectedDuration_min()))
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.EXPECTED_DURATION, TaskValueToString.expectedDurationToString(p_taskValue));
        }
        boolean bTaskTypeChanged = false;
        nodeLocal.setNodeValue(nodeValue);
        if (!p_taskValue.getType().equals("Anchor") && !p_taskValue.getTaskStateMachineValue().getPrimaryKey().equals(p_taskLocal.getTaskStateMachineLocal().getTaskStateMachineValue().getPrimaryKey())) {
            bTaskTypeChanged = true;
        }
        else {
            // check responsibility
            Collection responsibleBefore = p_taskLocal.getResponsibleContactablesLocal();
            Collection responsibleRemoved = new ArrayList(responsibleBefore);
            Collection currentResponsible = new ArrayList(p_responsibleContactableLocals);
            responsibleRemoved.removeAll(currentResponsible);
            Collection responsibleAdded = new ArrayList(currentResponsible);
            responsibleAdded.removeAll(responsibleBefore);
            Iterator i = responsibleRemoved.iterator();
            while(i.hasNext()) {
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.REMOVE_RESPONSIBILITY, ((ContactableLocal)i.next()).getNodeLocal().getName());
            }
            i = responsibleAdded.iterator();
            while(i.hasNext()) {
                p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_RESPONSIBILITY, ((ContactableLocal)i.next()).getNodeLocal().getName());
            }
            p_taskLocal.setResponsibleContactablesLocal(p_responsibleContactableLocals);
            NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(p_keyLocal, p_taskLocal, new HashSet(responsibleAdded));
        }
        // check interested
        Collection interestedBefore = p_taskLocal.getInterestedContactablesLocal();
        Collection interestedRemoved = new ArrayList(interestedBefore);
        Collection currentInterested = new ArrayList(p_interestedContactableLocals);
        interestedRemoved.removeAll(currentInterested);
        Collection interestedAdded = new ArrayList(currentInterested);
        interestedAdded.removeAll(interestedBefore);
        Iterator i = interestedRemoved.iterator();
        while(i.hasNext()) {
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.REMOVE_INTERESTED_CONTACTABLE, ((ContactableLocal)i.next()).getNodeLocal().getName());
        }
        i = interestedAdded.iterator();
        while(i.hasNext()) {
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_INTERESTED_CONTACTABLE, ((ContactableLocal)i.next()).getNodeLocal().getName());
        }
        p_taskLocal.setInterestedContactablesLocal(p_interestedContactableLocals);
        // copy the creator from the existing task so that it can't be changed
        p_taskValue.setCreatorContactableValue(p_taskLocal.getCreatorContactableLocal().getContactableValue());
        p_taskLocal.setTaskValue(p_taskValue);
        if (!p_taskLocal.getOwnerContactableLocal().getContactableId().equals(oldOwnerId))
            p_changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.OWNER, p_taskLocal.getOwnerContactableLocal().getNodeLocal().getName());
        if (bTaskTypeChanged)
            performStartTransitionLocal(p_keyLocal, p_taskLocal, p_changeDeltaLocal);
        else {
            NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, p_taskLocal, new HashSet(p_taskLocal.getInterestedContactablesLocal()));
        }
    }
    
    /**
     * Get the list of legal transitions to take from the specified task
     *
     * @param p_keyLocal User's security key
     * @param p_taskLocal The task that the transition list is required for
     *
     * @ejb.interface-method view-type="local"
     **/
    public Collection getLegalTransitionLocals(KeySessionLocal p_keyLocal,
                                               TaskLocal p_taskLocal)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTaskChangeState)) {
		StateLocal stateLocal = p_taskLocal.getStateLocal();
                TaskStateMachineLocal taskStateMachineLocal = p_taskLocal.getTaskStateMachineLocal();
                return TransitionUtil.getLocalHome().findBySourceStateAndTaskStateMachine(stateLocal, taskStateMachineLocal);
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(FinderException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Get the time of the last state change for the task
     *
     * @param p_keyLocal User's security key
     * @param p_taskLocal The task that the state change time is required for
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Date getTaskLastStateChangeTime(KeySessionLocal p_keyLocal,
                                                     TaskLocal p_taskLocal)
        throws PermissionDeniedException, InvalidKeyException {
        if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTaskChangeState)) {
            Date lastTime = new Date(0L);
            Iterator i = p_taskLocal.getNodeLocal().getDBObjectLocal().getChangeDeltasLocal().iterator();
            while(i.hasNext()) {
                ChangeDeltaLocal changeDeltaLocal = (ChangeDeltaLocal)i.next();
                if (changeDeltaLocal.getChangeDeltaTypeLocal().equals(m_changeDeltaTypeChangeState)) {
                    if (changeDeltaLocal.getDate().after(lastTime)) {
                        lastTime = changeDeltaLocal.getDate();
                    }
                }
            }
            return lastTime;
        }
        else
            throw new PermissionDeniedException();
    }
    
    /**
     * If the user has permission to change state on a specified task then go 
     * ahead and change it and log an appropriate ChangeDelta.
     *
     * @param p_keyLocal User's security key
     * @param p_taskLocal The task which is changing state
     * @param p_transitionLocal The value details for the state transition
     *
     * @ejb.interface-method view-type="local"
     **/
    public void changeState(KeySessionLocal p_keyLocal,
                            TaskLocal p_taskLocal,
                            TransitionLocal p_transitionLocal,
                            java.lang.String p_message,
                            java.lang.Byte p_percentageComplete,
                            java.io.File p_file,
                            java.lang.String p_contentType,
                            java.io.InputStream p_stream)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTaskChangeState)) {
                DBObjectLocal dBObjectLocal = DBObjectUtil.getLocalHome().create();
                StatusLocal statusLocal = p_transitionLocal.getStatusLocal();
                StateLocal destinationStateLocal = p_transitionLocal.getDestinationStateLocal();
                p_taskLocal.setStatusLocal(statusLocal);
                p_taskLocal.setStateLocal(destinationStateLocal);
                Set previousResponsibles = new HashSet(p_taskLocal.getResponsibleContactablesLocal());
                p_taskLocal.setResponsibleContactablesLocal(m_searchSessionLocal.getContactablesLocalAssignedToTransitionLocal(p_keyLocal, p_taskLocal, p_transitionLocal));
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.CHANGE_STATE, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATUS, statusLocal.getNodeLocal().getName());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATE, destinationStateLocal.getNodeLocal().getName());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.MESSAGE, p_message);
                if (destinationStateLocal.equals(m_startState)) {
                    p_taskLocal.setPercentageComplete(new Byte((byte)100));
                    p_taskLocal.setRemainingDuration_min(new Integer(0));
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERCENTAGE_COMPLETE, "100 (auto)");
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.EXPECTED_DURATION, "0 (auto)");
                    // create an empty time entry to set remaining to 0
                    TimeValue timeValue = new TimeValue();
                    timeValue.setDescription("Auto complete");
                    StateValue stateValue = new StateValue();
                    stateValue.setPrimaryKey((StatePK)(m_startState.getPrimaryKey()));
                    timeValue.setStateValue(stateValue);
                    Date tNow = new Date();
                    timeValue.setStartTime(tNow);
                    timeValue.setStopTime(tNow);
                    timeValue.setRemainingDuration_min(0);
                    timeValue.setPersonValue(p_keyLocal.getUserLocal().getPersonLocal().getPersonValue());
                    TimeLocal timeLocal = TimeUtil.getLocalHome().create(p_taskLocal, timeValue);
                    // the task has completed so update the cached dates
                    updateCachedDates(p_taskLocal, changeDeltaLocal.getDate().getTime(), changeDeltaLocal.getDate().getTime());
                }
                else {
                    if (!p_percentageComplete.equals(p_taskLocal.getPercentageComplete())) {
                        p_taskLocal.setPercentageComplete(p_percentageComplete);
                        changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERCENTAGE_COMPLETE, p_percentageComplete.toString());
                    }
                }
                FileAttachmentSessionUtil.getLocalHome().create().createFileAttachment(changeDeltaLocal, p_file, p_contentType, p_stream);
                Set responsibleContactablesLocal = new HashSet(p_taskLocal.getResponsibleContactablesLocal());
                Iterator i = responsibleContactablesLocal.iterator();
                while(i.hasNext()) {
                    ContactableLocal contactableLocal = (ContactableLocal)i.next();
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_RESPONSIBILITY, contactableLocal.getNodeLocal().getName());
                }
                Set newResponsibleContactablesLocal = new HashSet(responsibleContactablesLocal);
                newResponsibleContactablesLocal.removeAll(previousResponsibles);
                NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(p_keyLocal, p_taskLocal, newResponsibleContactablesLocal);
                NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, p_taskLocal, new HashSet(p_taskLocal.getInterestedContactablesLocal()));
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Enter the specified state machine for the task.
     *
     * @param p_key User's security key
     * @param p_task The task which is changing state
     *
     * @ejb.interface-method view-type="local"
     **/
    public void performStartTransitionLocal(KeySessionLocal p_keyLocal,
                                            TaskLocal p_taskLocal,
                                            ChangeDeltaLocal changeDeltaLocal)
        throws InvalidKeyException {
        try {
            TaskStateMachineLocal taskStateMachineLocal = p_taskLocal.getTaskStateMachineLocal();
            TransitionLocal startTransitionLocal = TransitionUtil.getLocalHome().findStartTransitionByTaskStateMachine(taskStateMachineLocal);
            p_taskLocal.setStateLocal(startTransitionLocal.getDestinationStateLocal());
            p_taskLocal.setStatusLocal(startTransitionLocal.getStatusLocal());
            Set previousResponsibles = new HashSet(p_taskLocal.getResponsibleContactablesLocal());
            p_taskLocal.setResponsibleContactablesLocal(m_searchSessionLocal.getContactablesLocalAssignedToTransitionLocal(p_keyLocal, p_taskLocal, startTransitionLocal));
            p_taskLocal.setInterestedContactablesLocal(m_searchSessionLocal.getContactablesLocalAssignedToTaskStateMachineLocal(p_keyLocal, p_taskLocal, taskStateMachineLocal));
            changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.TASKSTATEMACHINE, taskStateMachineLocal.getNodeLocal().getName());
            changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATE, p_taskLocal.getStateLocal().getNodeLocal().getName());
            changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATUS, p_taskLocal.getStatusLocal().getNodeLocal().getName());
            Set interestedContactablesLocal = new HashSet(p_taskLocal.getInterestedContactablesLocal());
            Iterator i = interestedContactablesLocal.iterator();
            while(i.hasNext()) {
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_INTERESTED_CONTACTABLE, ((ContactableLocal)i.next()).getNodeLocal().getName());
            }
            Set responsibleContactablesLocal = new HashSet(p_taskLocal.getResponsibleContactablesLocal());
            i = responsibleContactablesLocal.iterator();
            while(i.hasNext()) {
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_RESPONSIBILITY, ((ContactableLocal)i.next()).getNodeLocal().getName());
            }
            Set newResponsibleContactablesLocal = responsibleContactablesLocal;
            newResponsibleContactablesLocal.removeAll(previousResponsibles);
            NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(p_keyLocal, p_taskLocal, responsibleContactablesLocal);
            NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, p_taskLocal, interestedContactablesLocal);
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(FinderException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to reassign responsibility on a specified task 
     * then go ahead and change it and log an appropriate ChangeDelta.
     *
     * @param p_keyLocal User's security key
     * @param p_taskLocal The task which is changing responsibility
     * @param p_description The message associated with the change
     * @param p_contactablesLocal The value details for the new responsible contactables
     *
     * @ejb.interface-method view-type="local"
     **/
    public void reassignResponsibility(KeySessionLocal p_keyLocal,
                                       TaskLocal p_taskLocal,
                                       java.util.Collection p_contactablesLocal,
                                       java.lang.String p_message,
                                       java.io.File p_file,
                                       java.lang.String p_contentType,
                                       java.io.InputStream p_stream)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTaskReassignResponsibility)) {
                Set newResponsibleContactablesLocal = new HashSet(p_contactablesLocal);
                newResponsibleContactablesLocal.removeAll(p_taskLocal.getResponsibleContactablesLocal());
                p_taskLocal.setResponsibleContactablesLocal(p_contactablesLocal);
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.REASSIGN_RESPONSIBILITY, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.MESSAGE, p_message);
                FileAttachmentSessionUtil.getLocalHome().create().createFileAttachment(changeDeltaLocal, p_file, p_contentType, p_stream);
                Set responsibleContactablesLocal = new HashSet(p_taskLocal.getResponsibleContactablesLocal());
                Iterator i = responsibleContactablesLocal.iterator();
                while(i.hasNext()) {
                    ContactableLocal contactableLocal = (ContactableLocal)i.next();
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.RESPONSIBILITY, contactableLocal.getNodeLocal().getName());
                }
                NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(p_keyLocal, p_taskLocal, newResponsibleContactablesLocal);
                NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, p_taskLocal, new HashSet(p_taskLocal.getInterestedContactablesLocal()));
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    private int calculatePercentage(int totalTime_min, int remaining_min) {
        int percentage;
        if (totalTime_min + remaining_min != 0) {
            percentage = (totalTime_min * 100) / (totalTime_min + remaining_min);
            if (percentage < 0)
                percentage = 0;
            if (percentage > 100)
                percentage = 100;
        }
        else
            percentage = 100;
		return percentage;
    }
    
    private Byte calculateRemaining(KeySessionLocal p_keyLocal,
            				        TaskLocal p_taskLocal,
            				        Byte p_percentageComplete,
            				        com.velocityme.valueobjects.TimeValue p_timeValue) throws InvalidKeyException {
    	int percentage = 0;
    	int remaining_min = p_timeValue.getRemainingDuration_min().intValue();
    	int totalTime_min = getTotalTime_min(p_keyLocal, p_taskLocal);
    	// check for remaining time update
    	if (!p_timeValue.getRemainingDuration_min().equals(p_taskLocal.getRemainingDuration_min())) {
    		percentage = calculatePercentage(totalTime_min, remaining_min);
    	}
    	// else check for percentage complete update
    	else {
    		if (!p_percentageComplete.equals(p_taskLocal.getPercentageComplete())) {
    			percentage = p_percentageComplete.intValue();
    			if (percentage != 0)
    				remaining_min = (100 * totalTime_min) / percentage - totalTime_min;
    			else
    				remaining_min = p_taskLocal.getExpectedDuration_min().intValue();
    		}
   
        	// else calculate remainder and percentage given period
        	else {
        		long diff = p_timeValue.getStopTime().getTime() - p_timeValue.getStartTime().getTime();
        		int diff_min = (int)(diff / (1000 * 60));
        		remaining_min -= diff_min;
        		if (remaining_min < 0)
        			remaining_min = 0;
            	if (totalTime_min == 0) {
            		totalTime_min = p_taskLocal.getExpectedDuration_min().intValue() - remaining_min;
            		if (totalTime_min < 0)
            			totalTime_min = 0;
            	}
        		percentage = calculatePercentage(totalTime_min, remaining_min);
        	}
    	}
    	// round off to nearest 5
    	percentage = ((percentage + 3) / 5) * 5;
    	p_timeValue.setRemainingDuration_min(new Integer(remaining_min));
    	return new Byte((byte)percentage);
    }
    
    /**
     * Find the most recent time entry and then update remaining time for the task accordingly.
     * @param p_keyLocal 
     * @param p_taskLocal
     * @param p_percentageComplete
     * @throws NamingException
     * @throws CreateException
     */
    private void updateRemaining(KeySessionLocal p_keyLocal,
                                 TaskLocal p_taskLocal,
                                 Byte p_percentageComplete) 
            throws NamingException, CreateException {
        TimeLocal timeLocal = null;
        Iterator i = p_taskLocal.getTimesLocal().iterator();
        while(i.hasNext()) {
            TimeLocal tl = (TimeLocal)i.next();
            if (timeLocal == null || tl.getStopTime().after(timeLocal.getStopTime()))
                timeLocal = tl;
        }
        // is this the most recent time entry for the task?
        if (timeLocal != null) {
            if (p_percentageComplete != null && !p_percentageComplete.equals(p_taskLocal.getPercentageComplete())) {
                p_taskLocal.setPercentageComplete(p_percentageComplete);
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.EDIT_TIME, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERCENTAGE_COMPLETE, p_percentageComplete.toString());
            }
            if (!timeLocal.getRemainingDuration_min().equals(p_taskLocal.getRemainingDuration_min())) {
                p_taskLocal.setRemainingDuration_min(timeLocal.getRemainingDuration_min());
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.EDIT_TIME, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.EXPECTED_DURATION, TaskValueToString.expectedDurationToString(timeLocal.getRemainingDuration_min()));
            } 
        }
        
    }
    
    /**
     * If the user has permission to assign time on a specified task then go 
     * ahead.
     *
     * @param p_key User's security key
     * @param p_task The task
     * @param p_timeValue The value details for the time
     *
     * @ejb.interface-method view-type="local"
     **/
    public void assignTime(KeySessionLocal p_keyLocal,
                           TaskLocal p_taskLocal,
                           Byte p_percentageComplete,
                           com.velocityme.valueobjects.TimeValue p_timeValue)
        throws PermissionDeniedException, InvalidKeyException, InvalidTimeException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTimeCreate)) {
                if (!p_timeValue.getStartTime().before(p_timeValue.getStopTime()))
                    throw new InvalidTimeException();
            	p_percentageComplete = calculateRemaining(p_keyLocal, p_taskLocal, p_percentageComplete, p_timeValue);
                p_timeValue.setPersonValue(p_keyLocal.getUserLocal().getPersonLocal().getPersonValue());
                TimeLocal timeLocal = TimeUtil.getLocalHome().create(p_taskLocal, p_timeValue);
                // update the cached times by adding the new interval
                updateCachedDates(p_taskLocal, p_timeValue.getStartTime().getTime(), p_timeValue.getStopTime().getTime());
                updateRemaining(p_keyLocal, p_taskLocal, p_percentageComplete);
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to add time on a specified task then go 
     * ahead.
     *
     * @param p_key User's security key
     * @param p_task The task
     * @param p_timeValue The value details for the time
     *
     * @ejb.interface-method view-type="local"
     **/
    public void quickAddTime(KeySessionLocal p_keyLocal,
                             TaskLocal p_taskLocal,
                             int p_hours)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionTimeCreate)) {
                TimeValue timeValue = new TimeValue();
                timeValue.setDescription("Quick add");
                Calendar startTime = Calendar.getInstance();
                startTime.set(Calendar.HOUR_OF_DAY, 8);
                startTime.set(Calendar.MINUTE, 0);
                startTime.set(Calendar.SECOND, 0);
                timeValue.setStartTime(startTime.getTime());
                Calendar stopTime = (Calendar)startTime.clone();
                stopTime.set(Calendar.HOUR_OF_DAY, 8 + p_hours);
                timeValue.setStopTime(stopTime.getTime());
                timeValue.setTaskValue(p_taskLocal.getTaskValue());
                timeValue.setStateValue(p_taskLocal.getStateLocal().getStateValue());
                timeValue.setPersonValue(p_keyLocal.getUserLocal().getPersonLocal().getPersonValue());
                timeValue.setRemainingDuration_min(p_taskLocal.getRemainingDuration_min());
                TimeLocal timeLocal = TimeUtil.getLocalHome().create(p_taskLocal, timeValue);
                // update the cached times by adding the new interval
                updateCachedDates(p_taskLocal, timeValue.getStartTime().getTime(), timeValue.getStopTime().getTime());
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to edit time on a specified task then go 
     * ahead.
     *
     * @param p_key User's security key
     * @param p_timeLocal The existing time object
     * @param p_timeValue The value details for the time
     *
     * @ejb.interface-method view-type="local"
     **/
    public void editTime(KeySessionLocal p_keyLocal,
                         TimeLocal p_timeLocal,
                         Byte p_percentageComplete,
                         com.velocityme.valueobjects.TimeValue p_timeValue)
        throws PermissionDeniedException, InvalidKeyException, InvalidTimeException {
        try {
            TaskLocal taskLocal = p_timeLocal.getTaskLocal();
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, taskLocal.getNodeLocal()).contains(m_permissionTimeEdit)) {
                if (p_timeValue.getStartTime().before(p_timeValue.getStopTime())) {
                	p_percentageComplete = calculateRemaining(p_keyLocal, taskLocal, p_percentageComplete, p_timeValue);
                    ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.EDIT_TIME, p_keyLocal.getUserLocal(), taskLocal.getNodeLocal().getDBObjectLocal());
                    TimeValue oldValue = p_timeLocal.getTimeValue();
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERSON, p_timeValue.getPersonValue().getContactableValue().getNodeValue().getName());
                    if (!oldValue.getDescription().equals(p_timeValue.getDescription()))
                        changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, oldValue.getDescription() + " -> " + p_timeValue.getDescription());
                    if (!oldValue.getStateValue().getPrimaryKey().equals(p_timeValue.getStateValue().getPrimaryKey()))
                        changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATE, StateValueToString.toString(oldValue.getStateValue()) + " -> " + StateValueToString.toString(p_timeValue.getStateValue()));
                    if (!oldValue.getStartTime().equals(p_timeValue.getStartTime()))
                        changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.START_TIME, m_formatter.format(oldValue.getStartTime()) + " -> " + m_formatter.format(p_timeValue.getStartTime()));
                    if (!oldValue.getStopTime().equals(p_timeValue.getStopTime()))
                        changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STOP_TIME, m_formatter.format(oldValue.getStopTime()) + " -> " + m_formatter.format(p_timeValue.getStopTime()));
                    p_timeLocal.setTimeValue(p_timeValue);
                    // have to recalculate the cached interval
                    calculateCachedDates(taskLocal);
                    updateRemaining(p_keyLocal, taskLocal, p_percentageComplete);
                }
                else
                    throw new InvalidTimeException();
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to edit time on a specified task then go 
     * ahead and delete it.
     *
     * @param p_key User's security key
     * @param p_task The task
     * @param p_timeValue The value details for the time to delete
     *
     * @ejb.interface-method view-type="local"
     **/
    public void deleteTime(KeySessionLocal p_keyLocal,
                           TimeLocal p_timeLocal)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            TaskLocal taskLocal = p_timeLocal.getTaskLocal();
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, taskLocal.getNodeLocal()).contains(m_permissionTimeDelete)) {
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.DELETE_TIME, p_keyLocal.getUserLocal(), taskLocal.getNodeLocal().getDBObjectLocal());
                TimeValue oldValue = p_timeLocal.getTimeValue();
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PERSON, oldValue.getPersonValue().getContactableValue().getNodeValue().getName());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, oldValue.getDescription());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STATE, StateValueToString.toString(oldValue.getStateValue()));
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.START_TIME, m_formatter.format(oldValue.getStartTime()));
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.STOP_TIME, m_formatter.format(oldValue.getStopTime()));
                TimeUtil.getLocalHome().remove(p_timeLocal.getPrimaryKey());
                // have to recalculate the cached interval
                calculateCachedDates(taskLocal);
                updateRemaining(p_keyLocal, taskLocal, null);
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
        catch(RemoveException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Calculate the total time allocated to the task.
     * 
     * @param p_keyLocal User's security key
     * @param p_taskLocal Task that the time is required for
     * @return the total number of minutes allocated
     * 
     * @ejb.interface-method view-type="local"
     */
    public int getTotalTime_min(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal) throws InvalidKeyException {
    	int totalTime_min = 0;
    	if (p_keyLocal.isValid()) {
    		Collection times = p_taskLocal.getTimesLocal();
    		Iterator i = times.iterator();
    		while(i.hasNext()) {
    			TimeLocal timeLocal = (TimeLocal)i.next();
    			totalTime_min += timeLocal.getDuration().intValue() / 60000;
    		}
    	}
    	else
            throw new InvalidKeyException();
		return totalTime_min;
    }
    
    /**
     * Get the full list of time value objects for the given task.
     *
     * @param p_key User's security key
     * @param p_task Task that the times are required for
     * @param p_date Time after which to find time entries
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Collection getTimesValue(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal, Date p_date) throws InvalidKeyException {
        try {
            Collection values = new java.util.ArrayList();
            if (p_keyLocal.isValid()) {
                Collection times = TimeUtil.getLocalHome().findTaskTimeAfterDate(p_taskLocal, p_date);
                Iterator i = times.iterator();
                while(i.hasNext()) {
                    TimeLocal timeLocal = (TimeLocal)i.next();
                    values.add(timeLocal.getTimeValue());
                }
            }
            else
                throw new InvalidKeyException();
            return values;
        } catch(NamingException e) {
            throw new EJBException(e);
        } catch(FinderException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Toggle the interested state of the current user for the specified task
     *
     * @param p_key User's security key
     * @param p_task Task that the user is interested in
     *
     * @ejb.interface-method view-type="local"
     **/
    public void toggleInterestedState(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal) throws InvalidKeyException {
        try {
            if (p_keyLocal.isValid()) {
                Collection interestedContactablesLocal = p_taskLocal.getInterestedContactablesLocal();
                ContactableLocal meLocal = p_keyLocal.getUserLocal().getPersonLocal().getContactableLocal();
                if (interestedContactablesLocal.contains(meLocal)) {
                    interestedContactablesLocal.remove(meLocal);
                    p_taskLocal.setInterestedContactablesLocal(interestedContactablesLocal);
                    ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.INTERESTED, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.REMOVE_INTERESTED_CONTACTABLE, meLocal.getNodeLocal().getName());
                }
                else {
                    interestedContactablesLocal.add(meLocal);
                    p_taskLocal.setInterestedContactablesLocal(interestedContactablesLocal);
                    ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.INTERESTED, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_INTERESTED_CONTACTABLE, meLocal.getNodeLocal().getName());
                }
            }
            else
                throw new InvalidKeyException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Enable the interested state of the current user for the specified task
     *
     * @param p_key User's security key
     * @param p_task Task that the user is interested in
     *
     * @ejb.interface-method view-type="local"
     **/
    public void enableInterestedState(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal) throws InvalidKeyException {
        try {
            if (p_keyLocal.isValid()) {
                Collection interestedContactablesLocal = p_taskLocal.getInterestedContactablesLocal();
                ContactableLocal meLocal = p_keyLocal.getUserLocal().getPersonLocal().getContactableLocal();
                if (!interestedContactablesLocal.contains(meLocal)) {
                    interestedContactablesLocal.add(meLocal);
                    p_taskLocal.setInterestedContactablesLocal(interestedContactablesLocal);
                    ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.INTERESTED, p_keyLocal.getUserLocal(), p_taskLocal.getNodeLocal().getDBObjectLocal());
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.ADD_INTERESTED_CONTACTABLE, meLocal.getNodeLocal().getName());
                }
            }
            else
                throw new InvalidKeyException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Recursively walk the tree updating first the children then the current task
     *
     * @param p_key User's security key
     * @param p_task Task that is the root of the update
     *
     * @ejb.interface-method view-type="local"
     **/
    public void recursiveUpdateTimes(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal)
        throws PermissionDeniedException, InvalidKeyException {
        if (p_taskLocal.getType().equals("Project") && m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionProjectEdit)) {
            Date now = new Date();
            //internalRecursiveUpdateTimes(p_taskLocal, now.getTime());
            sortOutGraph(p_taskLocal, now.getTime());
        }
        else
           throw new PermissionDeniedException();
    }
    
    private class StartStopPair {
        private long m_start;
        private long m_stop;
        
        public StartStopPair(long p_start, long p_stop) {
            m_start = p_start;
            m_stop = p_stop;
        }
        public long getStart() {
            return m_start;
        }
        public long getStop() {
            return m_stop;
        }
        public void include(StartStopPair oth) {
            include(oth.m_start, oth.m_stop);
        }
        public void include(long p_start, long p_stop) {
            if (m_start > p_start)
                m_start = p_start;
            if (m_stop < p_stop)
                m_stop = p_stop;
        }
    }
    
    private void sortOutGraph(TaskLocal p_rootTaskLocal, long p_now) {
        TaskTreeGraph graph = new TaskTreeGraph();
        Collection dependencyLocals = new ArrayList();
        internalRecursiveTaskWalk(graph, p_rootTaskLocal, dependencyLocals, p_now);
        graph.mergeCurrentTime(p_rootTaskLocal, p_now);
        // add all the dependency vertices
        Iterator i = dependencyLocals.iterator();
        while(i.hasNext()) {
            TaskDependencyLocal dependency = (TaskDependencyLocal)i.next();
            graph.addStopStartVertex(dependency.getDependencyTaskLocal(), dependency.getDependantTaskLocal(), 0L);
        }
        // now we must process the graph
        while(!graph.isFinished()) {
            TaskTreeGraph.Node node = graph.getNextNode();
            if (node != null) {
                if (node instanceof TaskTreeGraph.StartNode) {
                    // the start of this task has been set so can calculate duration
                    TaskLocal taskLocal = node.getTaskLocal();
                    if (!taskLocal.getType().equals("Anchor")) {
                        long start = taskLocal.getStartDate().getTime();
                        long duration = 0L;
                        double expectedDuration_ms = taskLocal.getExpectedDuration_min().intValue() * 60000;
                        double fractionComplete = taskLocal.getPercentageComplete().byteValue() / 100.0;
                        if (start < p_now) {
                            double predictedByDuration = (p_now - start) + expectedDuration_ms * (1.0 - fractionComplete);
                            if (fractionComplete > 0.0) {
                                double predictedByComplete = (p_now - start) * (1.0 - fractionComplete) / fractionComplete;
                                duration = (long)((1.0 - fractionComplete) * predictedByDuration + fractionComplete * predictedByComplete);
                            }
                            else {
                                duration = (long)predictedByDuration;
                            }
                        }
                        else {
                            duration = (long)expectedDuration_ms;
                        }
                        graph.setDuration(node, duration);
                    }
                }
                graph.completeNode(node);
            }
            else
                return; // circular dependency
        }
    }
    
    private void internalRecursiveTaskWalk(TaskTreeGraph p_graph, TaskLocal p_taskLocal, Collection p_dependencyLocals, long p_now) {
        // add this task to the graph
        StartStopPair pair = getCachedDates(p_taskLocal);  // make sure that the cached dates have been computed
        p_graph.addTask(p_taskLocal);
        if (pair.getStart() == Long.MAX_VALUE)
            p_graph.mergeCurrentTime(p_taskLocal, p_now);
        // add any dependencies to the list
        p_dependencyLocals.addAll(p_taskLocal.getDependencyLinksLocal());
        Iterator i = p_taskLocal.getNodeLocal().getChildrenNodesLocal().iterator();
        while(i.hasNext()) {
            NodeLocal nodeLocal = (NodeLocal)i.next();
            if (nodeLocal.getDBObjectLocal().getIsDeleted().equals(Boolean.FALSE) &&
                nodeLocal.getType().equals("Task")) {
                TaskLocal childTaskLocal = nodeLocal.getTaskLocal();
                internalRecursiveTaskWalk(p_graph, childTaskLocal, p_dependencyLocals, p_now);
                Date taskDate = p_taskLocal.getCachedStartDate();
                Date childDate = childTaskLocal.getCachedStartDate();
                if (taskDate == null || (childDate != null && taskDate.after(childDate)))
                    p_taskLocal.setCachedStartDate(childDate);
                taskDate = p_taskLocal.getCachedStopDate();
                childDate = childTaskLocal.getCachedStopDate();
                if (taskDate == null || (childDate != null && taskDate.before(childDate)))
                    p_taskLocal.setCachedStopDate(childDate);
                // the child task is now in the graph so add vertices
                p_graph.addStartStartVertex(p_taskLocal, childTaskLocal, 0L);
                p_graph.addStopStopVertex(childTaskLocal, p_taskLocal, 0L);
            }
        }
        // create the duration vertex but indicate unitialized since we can't compute yet
        // Done after children so that the start time can be the minimum
        if (!p_taskLocal.getType().equals("Anchor"))
            p_graph.addStartStopVertex(p_taskLocal, p_taskLocal, -1L);
    }
    
/*    private StartStopPair internalRecursiveUpdateTimes(TaskLocal p_taskLocal, long p_now) {
        StartStopPair startStopPair = new StartStopPair(p_now, 0L);
        // Step 1: process all the children
        Iterator i = p_taskLocal.getNodeLocal().getChildrenNodesLocal().iterator();
        while(i.hasNext()) {
            NodeLocal nodeLocal = (NodeLocal)i.next();
            if (nodeLocal.getDBObjectLocal().getIsDeleted().equals(Boolean.FALSE) &&
                nodeLocal.getType().equals("Task")) {
                startStopPair.include(internalRecursiveUpdateTimes(nodeLocal.getTaskLocal(), p_now));
            }
        }
        if (!p_taskLocal.getType().equals("Anchor")) {
            // Step 2: include the cached start and stop times
            startStopPair.include(getCachedDates(p_taskLocal));
            // this is the earliest of the parents start date or the first allocated time to the task
            p_taskLocal.setStartDate(new Date(startStopPair.getStart()));
            // Step 3: determine the stop time for the task
            long start = startStopPair.getStart();
            long stop = startStopPair.getStop();
            // the task has completed if in start state and time has been allocated
            if (!(stop != p_now && p_taskLocal.getStateLocal().equals(m_startState))) {
                // calculate predicted stop time
                double predictedStop;
                double expectedDuration_ms = p_taskLocal.getExpectedDuration_min().intValue() * 60000;
                double fractionComplete = p_taskLocal.getPercentageComplete().byteValue() / 100.0;
                if (start < p_now) {
                    double predictedByDuration = p_now + expectedDuration_ms * (1.0 - fractionComplete);
                    double predictedByComplete = p_now + (p_now - start) * (1.0 - fractionComplete) / fractionComplete;
                    predictedStop = (1.0 - fractionComplete) * predictedByDuration + fractionComplete * predictedByComplete;
                }
                else {
                    predictedStop = start + expectedDuration_ms;
                }
                if (stop < predictedStop)
                    stop = (long)predictedStop;
            }
            // this is the latest of the stop dates
            p_taskLocal.setStopDate(new Date(stop));
            return new StartStopPair(start, stop);
        }
        else
            return new StartStopPair(p_taskLocal.getStartDate().getTime(), p_taskLocal.getStopDate().getTime());
    }*/
    
    /**
     * Check if the specified task is active
     *
     * @param p_key User's security key
     * @param p_task Task that is the root of the update
     *
     * @ejb.interface-method view-type="local"
     **/
    public boolean isActive(KeySessionLocal p_keyLocal, TaskLocal p_taskLocal)
        throws InvalidKeyException {
        if (p_keyLocal.isValid()) {
            if (p_taskLocal.getStateLocal() != null)
                return !p_taskLocal.getStateLocal().equals(m_startState);
            else
                return false;
        }
        else
            throw new InvalidKeyException();
    }
    
    private ProjectLocal getEncapsulatingProject(TaskLocal p_taskLocal) {
        ProjectLocal projectLocal = null;
        NodeLocal parentNodeLocal = p_taskLocal.getNodeLocal().getParentNodeLocal();
        while(parentNodeLocal != null) {
            if (parentNodeLocal.getFinalType().equals("Project")) {
                projectLocal = parentNodeLocal.getTaskLocal().getProjectLocal();
                break;
            }
            parentNodeLocal = parentNodeLocal.getParentNodeLocal();
        }
        return projectLocal;
    }
    
    /**
     * Gets a list of task dependencies that can be seen by this user from the 
     * specified task.
     *
     * @param p_key User's security key
     * @param p_node Current node in which the links will be found
     * @return A collection of node links that are visible from the specified node
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Collection getTaskDependencyValues(KeySessionLocal p_keyLocal,
                                                        TaskLocal p_taskLocal) throws InvalidKeyException {
        if (p_keyLocal.isValid()) {
            Collection taskDependencyValues = new ArrayList();
            Iterator i = p_taskLocal.getDependencyLinksLocal().iterator();
            while(i.hasNext()) {
                TaskDependencyLocal taskDependencyLocal = (TaskDependencyLocal)i.next();
                taskDependencyValues.add(taskDependencyLocal.getTaskDependencyValue());
            }
            return taskDependencyValues;
        }
        else
            throw new InvalidKeyException();
    }
    
    /**
     * Gets a list of task dependants that can be seen by this user from the 
     * specified task.
     *
     * @param p_key User's security key
     * @param p_node Current node in which the links will be found
     * @return A collection of node links that are visible from the specified node
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Collection getTaskDependantValues(KeySessionLocal p_keyLocal,
                                                       TaskLocal p_taskLocal) throws InvalidKeyException {
        if (p_keyLocal.isValid()) {
            Collection taskDependantValues = new ArrayList();
            Iterator i = p_taskLocal.getDependantLinksLocal().iterator();
            while(i.hasNext()) {
                TaskDependencyLocal taskDependencyLocal = (TaskDependencyLocal)i.next();
                taskDependantValues.add(taskDependencyLocal.getTaskDependencyValue());
            }
            return taskDependantValues;
        }
        else
            throw new InvalidKeyException();
    }
    
     /**
     * Checks to see if it is a legal dependency pair
     *
     * @param p_key User's security key
     * @param p_dependency The dependency task
     * @param p_dependant The dependant task
     *
     * @ejb.interface-method view-type="local"
     **/
    public boolean isValidDependencyPair(KeySessionLocal p_keyLocal, TaskLocal p_dependencyTaskLocal, TaskLocal p_dependantTaskLocal)
        throws InvalidKeyException {
        // make sure both tasks are actually valid tasks
        if (p_dependencyTaskLocal == null || p_dependantTaskLocal == null)
            return false;
        // make sure that they share the same encapsulating project
        ProjectLocal dependencyProjectLocal = getEncapsulatingProject(p_dependencyTaskLocal);
        ProjectLocal dependantProjectLocal = getEncapsulatingProject(p_dependantTaskLocal);
        if (dependencyProjectLocal == null || dependantProjectLocal == null ||
            !dependencyProjectLocal.equals(dependantProjectLocal))
            return false;
        return true;
    }
    
    /**
     * Links the task in the tree to the target task
     *
     * @param p_key User's security key
     * @param p_dependency The dependency task
     * @param p_dependant The dependant task
     *
     * @ejb.interface-method view-type="local"
     **/
    public void createTaskDependency(KeySessionLocal p_keyLocal, TaskLocal p_dependencyTaskLocal, TaskLocal p_dependantTaskLocal)
        throws InvalidKeyException, PermissionDeniedException {
        try {
            if (isValidDependencyPair(p_keyLocal, p_dependencyTaskLocal, p_dependantTaskLocal)) {
                TaskDependencyUtil.getLocalHome().create(p_dependencyTaskLocal, p_dependantTaskLocal);
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.CREATE_DEPENDENCY, p_keyLocal.getUserLocal(), p_dependencyTaskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DEPENDANT_TASK, p_dependantTaskLocal.getNodeLocal().getPathName());
                changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.CREATE_DEPENDENCY, p_keyLocal.getUserLocal(), p_dependantTaskLocal.getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DEPENDENCY_TASK, p_dependencyTaskLocal.getNodeLocal().getPathName());
            }
            else
                throw new PermissionDeniedException();
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Remove a dependency between a pair of tasks
     *
     * @param p_key User's security key
     * @param p_taskDependency The task dependency pair
     *
     * @ejb.interface-method view-type="local"
     **/
    public void deleteTaskDependency(KeySessionLocal p_keyLocal, TaskDependencyLocal p_taskDependencyLocal)
        throws InvalidKeyException, PermissionDeniedException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskDependencyLocal.getDependencyTaskLocal().getNodeLocal()).contains(m_permissionTaskEditDependency) &&
                m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_taskDependencyLocal.getDependantTaskLocal().getNodeLocal()).contains(m_permissionTaskEditDependency)) {
                // record the delete in both tasks
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.DELETE_DEPENDENCY, p_keyLocal.getUserLocal(), p_taskDependencyLocal.getDependencyTaskLocal().getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NODE, p_taskDependencyLocal.getDependantTaskLocal().getNodeLocal().getPathName());
                changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.DELETE_DEPENDENCY, p_keyLocal.getUserLocal(), p_taskDependencyLocal.getDependantTaskLocal().getNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NODE, p_taskDependencyLocal.getDependencyTaskLocal().getNodeLocal().getPathName());
                // now delete the dependency
                p_taskDependencyLocal.remove();
            }
            else
                throw new PermissionDeniedException();
        } catch(CreateException e) {
            throw new EJBException(e);
        } catch(NamingException e) {
            throw new EJBException(e);
        } catch(RemoveException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Get a tree model of tasks nested within the given root to given depth
     *
     * @param p_key User's security key
     *
     * @ejb.interface-method view-type="local"
     **/
    public javax.swing.tree.TreeModel getTaskValueTree(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                                       com.velocityme.interfaces.TaskLocal p_taskLocal,
                                                       int depth) throws InvalidKeyException {
        try {
            if (p_keyLocal.isValid()) {
                DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
                addTaskBranch(p_keyLocal, root, p_taskLocal, depth, false);
                return new DefaultTreeModel(root);
            }
            else
                throw new InvalidKeyException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    private void addTaskBranch(KeySessionLocal p_keyLocal,
                               DefaultMutableTreeNode p_parent,
                               TaskLocal p_taskLocal,
                               int p_depth,
                               boolean p_hasPermission) throws NamingException, CreateException, InvalidKeyException {
        if (p_depth > 0) {
            if (p_taskLocal.getNodeLocal().getDBObjectLocal().getIsDeleted().booleanValue() == false) {
                // if the user has permission to view the task then add it
                if (p_hasPermission || SearchSessionUtil.getLocalHome().create().getAssignedPermissionsLocal(p_keyLocal, p_taskLocal.getNodeLocal()).contains(m_permissionNodeView)) {
                    DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(p_taskLocal.getTaskValue());
                    p_parent.add(treeNode);
                    // first add each child if it is contactable
                    Iterator childIt = p_taskLocal.getNodeLocal().getChildrenNodesLocal().iterator();
                    while(childIt.hasNext()) {
                        NodeLocal childLocal = (NodeLocal)childIt.next();
                        if (childLocal.getType().equals("Task")) {
                            TaskLocal taskLocal = childLocal.getTaskLocal();
                            addTaskBranch(p_keyLocal, treeNode, taskLocal, p_depth-1, true);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Get the cached start and stop dates for a task. If they haven't already
     * been calculated then do it now!
     **/
    private StartStopPair getCachedDates(com.velocityme.interfaces.TaskLocal p_taskLocal) {
        if (p_taskLocal.getCachedStartDate() == null || p_taskLocal.getCachedStopDate() == null)
            calculateCachedDates(p_taskLocal);
        long start = p_taskLocal.getCachedStartDate() != null? p_taskLocal.getCachedStartDate().getTime(): Long.MAX_VALUE;
        long stop = p_taskLocal.getCachedStopDate() != null? p_taskLocal.getCachedStopDate().getTime(): Long.MIN_VALUE;
        return new StartStopPair(start, stop);
    }
    
    /**
     * Update the existing cached start and stop dates for a task by including
     * a new interval.
     **/
    private void updateCachedDates(com.velocityme.interfaces.TaskLocal p_taskLocal,
                                   long p_start,
                                   long p_stop) {
        StartStopPair taskInterval = getCachedDates(p_taskLocal);
        taskInterval.include(p_start, p_stop);
        p_taskLocal.setCachedStartDate(new Date(taskInterval.getStart()));
        p_taskLocal.setCachedStopDate(new Date(taskInterval.getStop()));
    }
    
    /**
     * Calculate the correct cached start and stop dates for a task based on
     * assigned times and state changed to start state if appropriate
     **/
    private void calculateCachedDates(com.velocityme.interfaces.TaskLocal p_taskLocal) {
        StartStopPair startStopPair = new StartStopPair(Long.MAX_VALUE, Long.MIN_VALUE);
        // iterate over allocated times to find earliest start and latest stop
        Iterator i = p_taskLocal.getTimesLocal().iterator();
        while(i.hasNext()) {
            TimeLocal timeLocal = (TimeLocal)i.next();
            startStopPair.include(timeLocal.getStartTime().getTime(), timeLocal.getStopTime().getTime());
        }
        // now check the state changes
        // only consider changes to Start State
        i = p_taskLocal.getNodeLocal().getDBObjectLocal().getChangeDeltasLocal().iterator();
        while(i.hasNext()) {
            ChangeDeltaLocal cdLocal = (ChangeDeltaLocal)i.next();
            if (cdLocal.getChangeDeltaTypeLocal().getChangeDeltaTypeId().intValue() == ChangeDeltaTypeBean.CHANGE_STATE) {
                Iterator j = cdLocal.getChangeDeltaItemsLocal().iterator();
                while(j.hasNext()) {
                    ChangeDeltaItemLocal cdiLocal = (ChangeDeltaItemLocal)j.next();
                    if (cdiLocal.getChangeDeltaItemTypeLocal().getChangeDeltaItemTypeId().intValue() == ChangeDeltaItemTypeBean.STATE &&
                        cdiLocal.getDescription().equals("Start State")) {
                        startStopPair.include(cdLocal.getDate().getTime(), cdLocal.getDate().getTime());
                    }
                }
            }
        }
        if (startStopPair.getStart() != Long.MAX_VALUE)
            p_taskLocal.setCachedStartDate(new Date(startStopPair.getStart()));
        else
            p_taskLocal.setCachedStartDate(null);
        if (startStopPair.getStop() != Long.MIN_VALUE)
            p_taskLocal.setCachedStopDate(new Date(startStopPair.getStop()));
        else
            p_taskLocal.setCachedStopDate(null);
    }
    
    
    /**
     * Create the Session Bean.
     * @throws CreateException 
     */
    public void ejbCreate() throws CreateException {
        m_formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm");
        try {
            m_searchSessionLocal = SearchSessionUtil.getLocalHome().create();
            m_permissionNodeView = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_VIEW)));
            m_permissionTaskChangeState = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TASK_CHANGE_STATE)));
            m_permissionTimeCreate = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TIME_CREATE)));
            m_permissionTimeEdit = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TIME_EDIT)));
            m_permissionTimeDelete = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TIME_DELETE)));
            m_permissionProjectEdit = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.PROJECT_EDIT)));
            m_permissionTaskReassignResponsibility = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TASK_REASSIGN_RESPONSIBILITY)));
            m_permissionTaskEditDependency = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.TASK_EDIT_DEPENDENCY)));
            m_changeDeltaTypeChangeState = ChangeDeltaTypeUtil.getLocalHome().findByPrimaryKey(new ChangeDeltaTypePK(new Integer(ChangeDeltaTypeBean.CHANGE_STATE)));
            m_startState = StateUtil.getLocalHome().findByName("Start State");
        }
        catch(NamingException e) {
            throw new CreateException(e.getMessage());
        }
        catch(FinderException e) {
            throw new CreateException(e.getMessage());
        }
    }

    public void ejbActivate() throws java.rmi.RemoteException {
    }
    
    public void ejbPassivate() throws java.rmi.RemoteException {
    }
    
    public void ejbRemove() throws java.rmi.RemoteException {
    }
    
    public void setSessionContext(javax.ejb.SessionContext sessionContext) throws java.rmi.RemoteException {
        m_context = sessionContext;
    }
    
}
